using System;
using System.Collections;
using Microsoft.SPOT;
using Microsoft.SPOT.Hardware;
using System.Threading;
using GHIElectronics.NETMF.FEZ;
using System.Runtime.CompilerServices;

namespace LCDLiveText
{
    // A delegate type for hooking up change notifications.
    public delegate bool ButtonEventHandler(object sender, ButtonEventArgs e);

    public partial class LCDLiveText
    {
        public ArrayList ButtonEventHandlerList;
        public enum ScrollDirection { Up, Down, Right, Left };
        enum Flags { none = 0, update = 1, flash = 2, scroll = 4 };

        #region Static Variables

        // visable area of LCD
        const byte LCD_Cols = FEZ_Shields.KeypadLCD.ROW_SIZE;
        const byte LCD_Rows = FEZ_Shields.KeypadLCD.MAX_ROWS;
        const byte VirtualRows = 4; // virtual screen size
        const byte VirtualCols = 40;//
        const int    cursorMax = VirtualRows * VirtualCols;

        byte[]  LCD_Data = new byte[cursorMax];
        byte[]      temp = new byte[LCD_Cols];  // holds copy of line for printing to LCD
        int       cursor = 0;            // position of cursor in the virtual screen
        byte    Offset_X = 0;            // offset of view port in X-axis
        byte    Offset_Y = 0;            // offset of view port in Y-axis
        byte  maxColUsed = 0;            // maximum column used
        byte  maxRowUsed = 0;            // maximum row used, used for smart scroll arrows
        int   updateCntr = 0;            // scales update timer for LCD refresh/flash events
        bool  scrollUpDn = false;        // need to know which scroll icons shoudl be shown
        bool  scrollLfRt = false;
        bool scrollState = false;        // track sroll icon state (on/off)

        bool flashState = false;         // track flash state (on/off)
        Flags     flags = Flags.none;    // used to track update state

        public event ButtonEventHandler ButtonEvent;
        ButtonEventArgs buttonArg;       // keep a global arg to keep from NEWing up every time
        FEZ_Shields.KeypadLCD.Keys currentButton;
        FEZ_Shields.KeypadLCD.Keys lastButton = FEZ_Shields.KeypadLCD.Keys.None; // for key down filter
        ExtendedTimer updateTimer;       // checks to see if LCD requires an update every 100ms

        // variables used by WaitForKey stuff
        FEZ_Shields.KeypadLCD.Keys WaitButton = FEZ_Shields.KeypadLCD.Keys.None;
        int WaitTimeout = 1; // number of update cycles to listen for
        bool WaitButtonFlag = false;
        bool WaitTimeOutFlag = false;

        #endregion

        #region Constructor

        /// <summary>
        /// Used to instantiate a single instance of the class, 
        /// or return an existing instance
        /// </summary>
        private static readonly LCDLiveText instance = new LCDLiveText();

        /// <summary>
        /// Public method to instantiate/get reference to only
        /// instance of this class.
        /// </summary>
        public static LCDLiveText Instance
        {
            get { return instance; }
        }

        /// <summary>
        /// Constructor, must use 'instance' to instantiate.
        /// </summary>
        private LCDLiveText()
        {
            Reset();
            buttonArg = new ButtonEventArgs(FEZ_Shields.KeypadLCD.Keys.None);
            ButtonEventHandlerList = new ArrayList();
            this.ButtonEvent += new ButtonEventHandler(ButtonEventRouter);
            updateTimer = new ExtendedTimer(new TimerCallback(updateTimerHandler), null, 1000, 100);
        }

        /// <summary>
        /// Resets LCD to initial conditions, 
        /// must reset before using.
        /// </summary>
        private void Reset()
        {
            FEZ_Shields.KeypadLCD.Initialize();
            FEZ_Shields.KeypadLCD.TurnBacklightOn(10);
            Clear();
        }

        #endregion

        #region Event Related Stuff

        /// <summary>
        /// Handles timer tick event, 
        /// looks for key press (fires event on keu up),
        /// and updates LCD if needed
        /// </summary>
        /// <param name="target"></param>
        void updateTimerHandler(object target)
        {
            currentButton = FEZ_Shields.KeypadLCD.GetKey();
            if (currentButton != FEZ_Shields.KeypadLCD.Keys.None
                & lastButton == FEZ_Shields.KeypadLCD.Keys.None)
            {
                lastButton = currentButton;
            }
            else if (currentButton == FEZ_Shields.KeypadLCD.Keys.None
                & lastButton != FEZ_Shields.KeypadLCD.Keys.None)
            {
                buttonArg.button = lastButton;
                lastButton = FEZ_Shields.KeypadLCD.Keys.None;
                OnButtonEvent(buttonArg);
            }

            // update every fifth timer tick
            if (flags > Flags.none)
            {
                if (updateCntr >= 5)
                {
                    if ((flags & Flags.flash) > 0)
                    {
                        flashState = !flashState;
                        flags |= Flags.update;
                    }
                    if ((flags & Flags.scroll) > 0)
                    {
                        scrollState = !scrollState ^ flashState; // alt scroll & flash
                        flags |= Flags.update;
                    }
                    updateCntr = 0;
                }

                if ((flags & Flags.update) > 0)
                {
                    flags &= (Flags)(0xFF ^ (int)Flags.update);
                    printBuffer();
                }
            }

            updateCntr++;
            if (WaitTimeout > 0) 
            { 
                WaitTimeout--;
                if (WaitTimeout < 1) { WaitTimeOutFlag = true; }
            }
        }

        /// <summary>
        /// Invokes the KeyEvent; called whenever list changes
        /// </summary>
        /// <param name="e">Key which is pressed</param>
        protected virtual void OnButtonEvent(ButtonEventArgs e)
        {
            if (ButtonEvent != null)
            {
                ButtonEvent(this, e);
            }
        }

        /// <summary>
        /// Button Pressed Event router
        /// </summary>
        /// <param name="sender">Object firing event</param>
        /// <param name="e">Button event</param>
        /// <returns>Bool, true == handled</returns>
        /// Iterates through list of registered Button event handlers from
        /// the bottom up. If a handler returns true then the event is 
        /// considedered handled and is not sent on up the list. 
        /// This provides a very simple routing scheme.
        private bool ButtonEventRouter(object sender, ButtonEventArgs e)
        {
            int count = ButtonEventHandlerList.Count;
            bool handled = false;
            //Debug.Print("Handlers = " + count.ToString());
            for (int i = count; i > 0 & handled == false; i--)
            {
                handled = (ButtonEventHandlerList[i - 1] as ButtonEventHandler)(sender, e);
            }
            
            return true;
        }

        /// <summary>
        /// A ButtonEvent handler that we add to tail end of
        /// ButtonEventHandlerList to listen for the a given button event.
        /// </summary>
        /// <param name="sender">Object firing event</param>
        /// <param name="e">Button event</param>
        /// <returns>True == handled</returns>
        private bool WaitForListener(object sender, ButtonEventArgs e)
        {
            if (e.button == WaitButton)
            {
                WaitButtonFlag = true;
                return true;
            }

            return false;
        }



        #endregion

        #region public interface

        /// <summary>
        /// Clear entire display area, and screen buffer
        /// </summary>
        [MethodImpl(MethodImplOptions.Synchronized)]
        public void Clear()
        {
            for (int i = 0; i < LCD_Data.Length; i++)
            {
                LCD_Data[i] = 32; // space ;
            }
            cursor = 0;
            Offset_X = 0;
            Offset_Y = 0;
            maxColUsed = 0;
            maxRowUsed = 0;
            flags = Flags.none;
            FEZ_Shields.KeypadLCD.Clear();
        }

        /// <summary>
        /// Prints at current cursor position
        /// </summary>
        /// <param name="str">String to print</param>
        /// Writes to buffer, sets updateFlag, adds CR
        [MethodImpl(MethodImplOptions.Synchronized)]
        public void Print(String str)
        {
            int start = cursor; // catch start > line end
            int length = ((str.Length + start) < cursorMax) ? str.Length : (cursorMax - cursor);
            int end = start + length;
            maxColUsed = (length > maxColUsed) ? (byte)length : maxColUsed;

            for (int i = 0; i < length; i++)
            {
                LCD_Data[cursor] = (byte)str[i];
                cursor++;
            }

            // new line
            cursor = (int)(((cursor - 1) / VirtualCols) + 1) * VirtualCols;
            maxRowUsed++;
            if (cursor >= cursorMax) { cursor = 0; } // roll data off top??

            updateScrollIcon();    //updateFlag = true;
            flags |= Flags.update;

        }

        /// <summary>
        /// Scroll display
        /// </summary>
        /// <param name="direction">Scroll direction</param>
        public void Scroll(ScrollDirection direction)
        {
            switch (direction)
            {
                case ScrollDirection.Up:
                    if (Offset_Y > 0) { Offset_Y--; }
                    break;
                case ScrollDirection.Down:
                    if (Offset_Y < VirtualRows - LCD_Rows) { Offset_Y++; }
                    break;
                case ScrollDirection.Right:
                    if (Offset_X < VirtualCols - LCD_Cols) { Offset_X++; }
                    break;
                case ScrollDirection.Left:
                    if (Offset_X > 0) { Offset_X--; }
                    break;
            }

            updateScrollIcon();
            flags |= Flags.update;
        }

        /// <summary>
        /// Gets/Sets display flash property
        /// </summary>
        public bool Flash
        {
            get
            {
                return ((flags & Flags.flash) > 0);
            }

            set
            {
                if (value)
                {
                    flags |= Flags.flash;  //set
                }
                else
                {
                    flags ^= Flags.flash; //clear
                }
            }
        }


        /// <summary>
        /// Pause execution of a thread until a given button is pressed
        /// or a timeout occures.
        /// </summary>
        /// <param name="button">The button to wait for</param>
        /// <param name="time">The amount of time to wait, in update scans (100ms)</param>
        /// <returns>True if button pressed, flase if timeout</returns>
        public bool WaitForButton(FEZ_Shields.KeypadLCD.Keys button, int time)
        {
            // reset variables and add listener to handlerm list
            WaitButtonFlag = false;
            WaitTimeOutFlag = false;
            WaitButton = button;
            WaitTimeout = time;
            ButtonEventHandlerList.Add(new ButtonEventHandler(WaitForListener));

            while (!WaitButtonFlag & !WaitTimeOutFlag)
            {
                Thread.Sleep(100);
            }

            // assum eno other handler has been added while we were waiting
            ButtonEventHandlerList.RemoveAt(ButtonEventHandlerList.Count - 1);
            return WaitButtonFlag ? true : false;
        }

        #endregion

        #region helpers

        /// <summary>
        /// Print the viewable area from buffer to LCD
        /// </summary>
        /// called by updateTimerHandler
        void printBuffer()
        {
            for (byte i = 0; i < LCD_Rows; i++)
            {
                copyBuffer(i);
                FEZ_Shields.KeypadLCD.SetCursor(i, 0);
                FEZ_Shields.KeypadLCD.Print(temp);
            }
        }

        /// <summary>
        /// Helper function to copy one line from buffer to a screen
        /// sized array sent to LCD print routine
        /// </summary>
        /// <param name="row">Physical LCD row</param>
        [MethodImpl(MethodImplOptions.Synchronized)]
        void copyBuffer(byte row)
        {
            int start = Offset_X + (Offset_Y + row) * VirtualCols;
            int end = start + FEZ_Shields.KeypadLCD.ROW_SIZE;
            int EOL = start + VirtualCols - Offset_X;

            for (int i = 0; i < LCD_Cols; i++) // should inc to LCD width
            {
                temp[i] = (i < EOL & !flashState) ? LCD_Data[start + i] : (byte)32;
            }
            if (scrollState)
            {
                if (row == 0 & scrollLfRt) { temp[LCD_Cols - 1] = (byte)(1); }
                if (row == 1 & scrollUpDn) { temp[LCD_Cols - 1] = (byte)(2); }
            }
        }

        /// <summary>
        /// Updates the SmartScroll icon
        /// </summary>
        /// Called anytime the data in the LCD_data buffer is changed.
        /// The scroll icon is changed to indicate which directions
        /// can be scrolled to from current screen view.
        void updateScrollIcon()
        {
            bool leftScroll = Offset_X > 0;
            bool rightScroll = maxColUsed > Offset_X + 16;
            bool upScroll = Offset_Y > 0;
            bool downScroll = maxRowUsed > Offset_Y + 2;

            if (leftScroll | rightScroll | upScroll | downScroll)
            {
                flags |= Flags.scroll;
            }
            else
            {
                flags &= (Flags)(0xFF ^ (int)Flags.scroll);
            }
            //updateFlag |= scrollFlag;
            scrollLfRt = false;
            scrollUpDn = false;

            if (leftScroll | rightScroll)
            {
                scrollLfRt = true;
                if (leftScroll & rightScroll)
                {
                    FEZ_Shields.KeypadLCD.genChar(1, LCDLiveText.leftRightScroll);
                }
                else if (rightScroll)
                {
                    FEZ_Shields.KeypadLCD.genChar(1, LCDLiveText.rightScroll);
                }
                else
                {
                    FEZ_Shields.KeypadLCD.genChar(1, LCDLiveText.leftScroll);
                }
            }

            if (upScroll | downScroll)
            {
                scrollUpDn = true;
                if (upScroll & downScroll)
                {
                    FEZ_Shields.KeypadLCD.genChar(2, LCDLiveText.upDownScroll);
                }
                else if (downScroll)
                {
                    FEZ_Shields.KeypadLCD.genChar(2, LCDLiveText.downScroll);
                }
                else
                {
                    FEZ_Shields.KeypadLCD.genChar(2, LCDLiveText.upScroll);
                }
            }

        }

        #endregion helpers

    }

    #region ButtonEventArgs helper class

    /// <summary>
    /// This classed used to pass KeyPressed Events
    /// </summary>
    public class ButtonEventArgs : EventArgs
    {
        public ButtonEventArgs(FEZ_Shields.KeypadLCD.Keys button)
        {
            this.button = button;
        }

        public FEZ_Shields.KeypadLCD.Keys button;

    }

    #endregion
}